<?php

/**
 * StrToBcmath.php
 * 版本更新记录：
 * - [20250720] 兼容|BUG：支持负数的运算，解决负数计算 bug。
 * - [20250720] 兼容|BUG：兼容连续'-'、连续'+'、混合'+-'连续等情况。
 * - [20250720] 兼容支持小数指数的近似运算，使用 pow 函数处理小数指数。
 * - [20250720] 兼容：解决指数运算底与幂是小数、负数、多余'+'的情况。
 * - [20250805] 优化：增加maxScale传参，计算过程中使用最大精度计算（默认30位），保障最终结果更加准确。
 */

namespace k6xiao;

// 字符串转BcMath计算结果
class StrToBcmath {
    /**
     * @var int
     */
    private $scale; // 精度
    /**
     * @var mixed
     */
    private $maxScale; // 最大精度

    /**
     * @var mixed   // 是否输出计算过程
     */
    private $isecho;

    /**
     * @param $scale    // 可选，精度，默认值：8
     * @param $isecho   // 可选，是否输出计算过程，默认值：false
     * @param $maxScale   // 可选，最大精度，计算过程中使用的精度，保障结果更准确，默认值 30
     */
    public function __construct($scale = 8, $isecho = false, $maxScale = 30) {
        if (!is_numeric($scale)) {
            throw new \InvalidArgumentException('精度必须为数字：' . $scale);
        }
        if (!is_bool($isecho)) {
            throw new \InvalidArgumentException('是否输出计算过程必须为布尔值：' . $isecho);
        }
        if (!is_numeric($maxScale)) {
            throw new \InvalidArgumentException('最大精度必须为数字：' . $maxScale);
        }
        if ($scale > $maxScale) {
            $maxScale = $scale;
        }
        $this->scale    = $scale;
        $this->isecho   = $isecho;
        $this->maxScale = $maxScale;
    }

    // 主方法
    /**
     * @param $expression   // 复杂运算表达式
     * @param $scale    // 精度
     * @param $isRecursive  // 是否递归，递归时不输出计算过程
     */
    public function of($expression, $scale = '', $isRecursive = false) {
        $scale    = $scale ?: $this->scale;
        $maxScale = $this->maxScale; // 计算过程使用最大精度

        // 原始表达式
        if (!$isRecursive && $this->isecho) {
            echo $expression . '<br/>';
        }

        // 去除空格
        if (!$isRecursive) {
            $expression = str_replace(' ', '', $expression);
        }

        // 处理连续加号
        $originalExpression = $expression;
        $expression         = preg_replace('/(\+{2,})/', '+', $expression);
        if ($originalExpression !== $expression && $this->isecho) {
            echo '=' . $expression . '<br/>';
        }

        // 移除操作数前多余的加号
        $originalExpression = $expression;
        $expression         = preg_replace('/([+\-*\/\(]|^)\+/', '$1', $expression);
        if ($originalExpression !== $expression && $this->isecho) {
            echo '=' . $expression . '<br/>';
        }

        // 处理连续负号
        $originalExpression = $expression;
        $expression         = preg_replace_callback('#([+\-*/(]|\*\*|^)(\-{2,})#', function ($matches) {
            // 判断连续负号的数量是奇数还是偶数
            $minusCount  = strlen($matches[2]);
            $newOperator = ($minusCount % 2 === 0) ? '+' : '-';
            return $matches[1] . $newOperator;
        }, $expression);
        if ($originalExpression !== $expression && $this->isecho) {
            echo '=' . $expression . '<br/>';
        }

        // 去除减号后面多余的加号
        $originalExpression = $expression;
        $expression         = preg_replace('/-(\++)/', '-', $expression);
        if ($originalExpression !== $expression && $this->isecho) {
            echo '=' . $expression . '<br/>';
        }

        // 如果输入的表达式为空或者包含非法字符，则抛出异常
        if (empty($expression) || preg_match('/[^0-9\.\+\-\*\/\(\)]/', $expression)) {
            throw new \InvalidArgumentException('表达式错误，仅支持+,-,*,/,**,(),数字：' . $expression);
        }

        // 使用正则表达式匹配表达式中的括号
        while (preg_match('/\(([^\(\)]+)\)/', $expression, $matches)) {
            // 计算括号中的表达式
            $res = $this->of($matches[1], $maxScale, true);
            // 将没有括号的表达式替换到原表达式中
            $expression = str_replace('(' . $matches[1] . ')', $res, $expression);
            if ($this->isecho) {
                echo '=' . $expression . '<br/>';
            }
        }

        // 处理指数运算
        while (preg_match('/(-?\d+(\.\d+)?)(\*\*)(-?\d+(\.\d+)?)/', $expression, $matches)) {
            $res        = $this->basic($matches[0], $maxScale);
            $expression = str_replace($matches[0], $res, $expression);
            if (!$isRecursive && $this->isecho) {
                echo '=' . $expression . '<br/>';
            }
        }

        // 处理乘法、除法
        while (preg_match('/(-?\d+(\.\d+)?)([\/*])(-?\d+(\.\d+)?)/', $expression, $matches)) {
            $res        = $this->basic($matches[0], $maxScale);
            $expression = str_replace($matches[0], $res, $expression);
            if (!$isRecursive && $this->isecho) {
                echo '=' . $expression . '<br/>';
            }
        }
        // 处理加法和减法
        while (preg_match('/(-?\d+(\.\d+)?)([+\-])(-?\d+(\.\d+)?)/', $expression, $matches)) {
            if (preg_match('/^[-+]\d+(\.\d+)?$/', $matches[0])) {
                break; // 避免将负数符号误识别为减号
            }
            $res        = $this->basic($matches[0], $maxScale);
            $expression = str_replace($matches[0], $res, $expression);
            if (!$isRecursive && $this->isecho) {
                echo '=' . $expression . '<br/>';
            }
        }
        // 返回计算结果
        return bcadd($expression, '0', $scale);
    }

    // 基本的四则运算
    /**
     * @param $expression   // 基本的四则运算表达式
     * @param $scale    // 精度
     */
    public function basic($expression, $scale = '') {
        $scale = $scale ?: $this->scale;
        // 使用正则表达式匹配表达式中的两个操作数和一个运算符
        if (!preg_match('/^(-?\d+(\.\d+)?)([\/*+-]|(\*\*))(-?\d+(\.\d+)?)$/', $expression, $matches)) {
            throw new \InvalidArgumentException('表达式错误，不符合基本的四则运算：' . $expression);
        }
        // dump($matches);
        // 根据运算符调用对应的 bcmath 函数
        $operator = $matches[3];
        $a        = strval($matches[1]);
        $b        = strval($matches[5]);
        if ($this->isecho) {
            // echo '【处理运算符】' . $a . $operator . $b . '<br/>';
        }
        try {
            switch ($operator) {
            case '+':
                return bcadd($a, $b, $scale);
            case '-':
                return bcsub($a, $b, $scale);
            case '*':
                return bcmul($a, $b, $scale);
            case '/':
                if ($b == 0) {
                    throw new \InvalidArgumentException('除数不能为零：' . $a . $operator . $b);
                }
                return bcdiv($a, $b, $scale);
            case '**':
                // 先处理整数形式的小数指数，如 2.0 转为 2
                if (strpos($b, '.') !== false && (float) $b === (int) $b) {
                    $b = (int) $b;
                }
                // 处理底数为 0 且指数为负数的情况
                if (bccomp($a, '0', $scale) === 0 && bccomp($b, '0', $scale) < 0) {
                    throw new \InvalidArgumentException('0 的负数次幂无意义：' . $a . $operator . $b);
                }
                // 处理负数底数且非整数指数的情况
                if (strpos($a, '-') !== false && strpos($b, '.') !== false && (float) $b != (int) $b) {
                    throw new \InvalidArgumentException('负数底数的非整数次幂在实数范围内无意义：' . $a . $operator . $b);
                }

                // 处理非整数指数使用pow函数近似
                if (strpos($b, '.') !== false) {
                    $result = pow($a, $b);
                    return bcadd($result, '0', $scale);
                }
                return bcpow($a, $b, $scale);
            default:
                throw new \InvalidArgumentException('未知的运算符：' . $a . $operator . $b);
            }
        } catch (\Exception $e) {
            throw new \InvalidArgumentException($e->getMessage());
        }
    }

    // 重设精度
    /**
     * @param $scale
     */
    public function setScale($scale) {
        if (!is_numeric($scale)) {
            throw new \InvalidArgumentException('精度必须为数字：' . $scale);
        }
        $this->scale = $scale;
    }

    // 重设是否输出计算过程
    /**
     * @param $isecho
     */
    public function setIsecho($isecho) {
        if (!is_bool($isecho)) {
            throw new \InvalidArgumentException('是否输出计算过程必须为布尔值：' . $isecho);
        }
        $this->isecho = $isecho;
    }

}